// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

package com.android.tools.r8.graph;

import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AbstractAccessContexts.ConcreteAccessContexts;
import com.android.tools.r8.utils.collections.ProgramMethodSet;
import com.google.common.collect.Sets;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 * Holds whole program information about the usage of a given field.
 *
 * <p>The information is generated by the {@link com.android.tools.r8.shaking.Enqueuer}.
 */
public class FieldAccessInfoImpl implements FieldAccessInfo {

  public static final FieldAccessInfoImpl MISSING_FIELD_ACCESS_INFO = new FieldAccessInfoImpl(null);

  public static int FLAG_IS_READ_FROM_ANNOTATION = 1 << 0;
  public static int FLAG_IS_READ_FROM_METHOD_HANDLE = 1 << 1;
  public static int FLAG_IS_WRITTEN_FROM_METHOD_HANDLE = 1 << 2;
  public static int FLAG_HAS_REFLECTIVE_ACCESS = 1 << 3;

  // A direct reference to the definition of the field.
  private DexField field;

  // If this field is accessed from a method handle or has a reflective access.
  private int flags;

  // Maps every direct and indirect reference in a read-context to the set of methods in which that
  // reference appears.
  private AbstractAccessContexts readsWithContexts = AbstractAccessContexts.empty();

  // Maps every direct and indirect reference in a write-context to the set of methods in which that
  // reference appears.
  private AbstractAccessContexts writesWithContexts = AbstractAccessContexts.empty();

  public FieldAccessInfoImpl(DexField field) {
    this.field = field;
  }

  void destroyAccessContexts() {
    readsWithContexts = AbstractAccessContexts.unknown();
    writesWithContexts = AbstractAccessContexts.unknown();
  }

  void flattenAccessContexts() {
    flattenAccessContexts(readsWithContexts);
    flattenAccessContexts(writesWithContexts);
  }

  private void flattenAccessContexts(AbstractAccessContexts accessesWithContexts) {
    accessesWithContexts.flattenAccessContexts(field);
  }

  @Override
  public FieldAccessInfoImpl asMutable() {
    return this;
  }

  @Override
  public DexField getField() {
    return field;
  }

  public AbstractAccessContexts getReadsWithContexts() {
    return readsWithContexts;
  }

  public void setReadsWithContexts(AbstractAccessContexts readsWithContexts) {
    this.readsWithContexts = readsWithContexts;
  }

  public void setWritesWithContexts(AbstractAccessContexts writesWithContexts) {
    this.writesWithContexts = writesWithContexts;
  }

  @Override
  public int getNumberOfReadContexts() {
    return readsWithContexts.getNumberOfAccessContexts();
  }

  @Override
  public int getNumberOfWriteContexts() {
    return writesWithContexts.getNumberOfAccessContexts();
  }

  @Override
  public ProgramMethod getUniqueReadContext() {
    return readsWithContexts.isConcrete()
        ? readsWithContexts.asConcrete().getUniqueAccessContext()
        : null;
  }

  @Override
  public boolean hasKnownWriteContexts() {
    return !writesWithContexts.isTop();
  }

  @Override
  public void forEachIndirectAccess(Consumer<DexField> consumer) {
    // There can be indirect reads and writes of the same field reference, so we need to keep track
    // of the previously-seen indirect accesses to avoid reporting duplicates.
    Set<DexField> visited = Sets.newIdentityHashSet();
    forEachIndirectAccess(consumer, readsWithContexts, visited);
    forEachIndirectAccess(consumer, writesWithContexts, visited);
  }

  private void forEachIndirectAccess(
      Consumer<DexField> consumer,
      AbstractAccessContexts accessesWithContexts,
      Set<DexField> visited) {
    if (accessesWithContexts.isBottom()) {
      return;
    }
    if (accessesWithContexts.isConcrete()) {
      accessesWithContexts
          .asConcrete()
          .forEachAccess(consumer, access -> access != field && visited.add(access));
      return;
    }
    throw new Unreachable("Should never be iterating the indirect accesses when they are unknown");
  }

  @Override
  public void forEachIndirectAccessWithContexts(BiConsumer<DexField, ProgramMethodSet> consumer) {
    Map<DexField, ProgramMethodSet> indirectAccessesWithContexts = new IdentityHashMap<>();
    addAccessesWithContextsToMap(
        readsWithContexts, access -> access != field, indirectAccessesWithContexts);
    addAccessesWithContextsToMap(
        writesWithContexts, access -> access != field, indirectAccessesWithContexts);
    indirectAccessesWithContexts.forEach(consumer);
  }

  private static void addAccessesWithContextsToMap(
      AbstractAccessContexts accessesWithContexts,
      Predicate<DexField> predicate,
      Map<DexField, ProgramMethodSet> out) {
    if (accessesWithContexts.isBottom()) {
      return;
    }
    if (accessesWithContexts.isConcrete()) {
      extendAccessesWithContexts(
          accessesWithContexts.asConcrete().getAccessesWithContexts(), predicate, out);
      return;
    }
    throw new Unreachable("Should never be iterating the indirect accesses when they are unknown");
  }

  private static void extendAccessesWithContexts(
      Map<DexField, ProgramMethodSet> accessesWithContexts,
      Predicate<DexField> predicate,
      Map<DexField, ProgramMethodSet> out) {
    accessesWithContexts.forEach(
        (access, contexts) -> {
          if (predicate.test(access)) {
            out.computeIfAbsent(access, ignore -> ProgramMethodSet.create()).addAll(contexts);
          }
        });
  }

  @Override
  public void forEachReadContext(Consumer<ProgramMethod> consumer) {
    readsWithContexts.forEachAccessContext(consumer);
  }

  @Override
  public void forEachWriteContext(Consumer<ProgramMethod> consumer) {
    writesWithContexts.forEachAccessContext(consumer);
  }

  @Override
  public boolean hasReflectiveAccess() {
    return (flags & FLAG_HAS_REFLECTIVE_ACCESS) != 0;
  }

  public void setHasReflectiveAccess() {
    flags |= FLAG_HAS_REFLECTIVE_ACCESS;
  }

  /** Returns true if this field is read by the program. */
  @Override
  public boolean isRead() {
    return !readsWithContexts.isEmpty() || isReadFromAnnotation() || isReadFromMethodHandle();
  }

  @Override
  public boolean isReadFromAnnotation() {
    return (flags & FLAG_IS_READ_FROM_ANNOTATION) != 0;
  }

  public void setReadFromAnnotation() {
    flags |= FLAG_IS_READ_FROM_ANNOTATION;
  }

  @Override
  public boolean isReadFromMethodHandle() {
    return (flags & FLAG_IS_READ_FROM_METHOD_HANDLE) != 0;
  }

  public void setReadFromMethodHandle() {
    flags |= FLAG_IS_READ_FROM_METHOD_HANDLE;
  }

  /** Returns true if this field is written by the program. */
  @Override
  public boolean isWritten() {
    return !writesWithContexts.isEmpty();
  }

  @Override
  public boolean isWrittenFromMethodHandle() {
    return (flags & FLAG_IS_WRITTEN_FROM_METHOD_HANDLE) != 0;
  }

  public void setWrittenFromMethodHandle() {
    flags |= FLAG_IS_WRITTEN_FROM_METHOD_HANDLE;
  }

  /**
   * Returns true if this field is written by a method for which {@param predicate} returns true.
   */
  @Override
  public boolean isWrittenInMethodSatisfying(Predicate<ProgramMethod> predicate) {
    return writesWithContexts.isAccessedInMethodSatisfying(predicate);
  }

  /**
   * Returns true if this field is only written by methods for which {@param predicate} returns
   * true.
   */
  @Override
  public boolean isWrittenOnlyInMethodSatisfying(Predicate<ProgramMethod> predicate) {
    return writesWithContexts.isAccessedOnlyInMethodSatisfying(predicate);
  }

  /**
   * Returns true if this field is only read by methods for which {@param predicate} returns true.
   */
  @Override
  public boolean isReadOnlyInMethodSatisfying(Predicate<ProgramMethod> predicate) {
    return readsWithContexts.isAccessedOnlyInMethodSatisfying(predicate);
  }

  /**
   * Returns true if this field is written by a method in the program other than {@param method}.
   */
  @Override
  public boolean isWrittenOutside(DexEncodedMethod method) {
    return writesWithContexts.isAccessedOutside(method);
  }

  public boolean recordRead(DexField access, ProgramMethod context) {
    if (readsWithContexts.isBottom()) {
      readsWithContexts = new ConcreteAccessContexts();
    }
    if (readsWithContexts.isConcrete()) {
      return readsWithContexts.asConcrete().recordAccess(access, context);
    }
    return false;
  }

  public boolean recordWrite(DexField access, ProgramMethod context) {
    if (writesWithContexts.isBottom()) {
      writesWithContexts = new ConcreteAccessContexts();
    }
    if (writesWithContexts.isConcrete()) {
      return writesWithContexts.asConcrete().recordAccess(access, context);
    }
    return false;
  }

  public void clearReads() {
    readsWithContexts = AbstractAccessContexts.empty();
  }

  public void clearWrites() {
    writesWithContexts = AbstractAccessContexts.empty();
  }

  public FieldAccessInfoImpl rewrittenWithLens(DexDefinitionSupplier definitions, GraphLens lens) {
    FieldAccessInfoImpl rewritten = new FieldAccessInfoImpl(lens.lookupField(field));
    rewritten.flags = flags;
    rewritten.readsWithContexts = readsWithContexts.rewrittenWithLens(definitions, lens);
    rewritten.writesWithContexts = writesWithContexts.rewrittenWithLens(definitions, lens);
    return rewritten;
  }

  public FieldAccessInfoImpl join(FieldAccessInfoImpl impl) {
    FieldAccessInfoImpl merged = new FieldAccessInfoImpl(field);
    merged.flags = flags | impl.flags;
    merged.readsWithContexts = readsWithContexts.join(impl.readsWithContexts);
    merged.writesWithContexts = writesWithContexts.join(impl.writesWithContexts);
    return merged;
  }
}
